昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。
想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。
今天結束後,你將學會:
TDD 的核心是一個簡單而強大的三步循環:
🔴 紅燈(Red) ➜ 🟢 綠燈(Green) ➜ 🔵 重構(Refactor)
↑ ↓
← ← ← ← ← ← ← ← ← ← ← ← ← ← ←
紅燈階段的核心思想:先思考需求,再動手寫程式
建立 tests/day03/math-utils.test.ts
:
import { describe, it, expect } from 'vitest'
describe('math utilities', () => {
describe('isPrime prime detection', () => {
it('identifies small prime numbers', () => {
// 還沒有 isPrime 函數,所以這個測試會失敗(紅燈)
expect(isPrime(2)).toBe(true)
expect(isPrime(3)).toBe(true)
expect(isPrime(5)).toBe(true)
})
it('identifies small composite numbers', () => {
expect(isPrime(4)).toBe(false)
expect(isPrime(6)).toBe(false)
expect(isPrime(9)).toBe(false)
})
})
})
執行測試:
npm test -- day03
預期結果:測試失敗,因為 isPrime
函數還不存在。這就是我們要的「紅燈」!
綠燈階段的核心思想:用最簡單的方法讓測試通過
建立 src/math/mathUtils.ts
:
export function isPrime(n: number): boolean {
// 最簡單的實作:硬編碼我們測試的數字
if (n === 2 || n === 3 || n === 5) {
return true
}
if (n === 4 || n === 6 || n === 9) {
return false
}
return false // 其他數字先回傳 false
}
更新測試檔,加入 import:
import { describe, it, expect } from 'vitest'
import { isPrime } from '../../src/math/mathUtils'
// ... 測試內容保持不變
執行測試:
npm test -- day03
結果:測試通過!我們達到了綠燈階段。
重構階段的核心思想:在測試保護下,改善代碼品質
我們的硬編碼實作太醜了,讓我們重構:
export function isPrime(n: number): boolean {
// 處理邊界情況
if (n < 2) return false
if (n === 2) return true
if (n % 2 === 0) return false
// 檢查奇數因子到 sqrt(n)
for (let i = 3; i * i <= n; i += 2) {
if (n % i === 0) return false
}
return true
}
執行測試確認重構成功:
npm test -- day03
測試仍然通過!重構成功。
讓我們做第二輪循環,增加邊界情況的測試:
it('handles boundary cases', () => {
expect(isPrime(0)).toBe(false)
expect(isPrime(1)).toBe(false)
expect(isPrime(-1)).toBe(false)
})
it('handles larger prime numbers', () => {
expect(isPrime(11)).toBe(true)
expect(isPrime(13)).toBe(true)
})
執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。
TDD 不只是技術,更是一種開發節奏:
看到這些「代碼異味」就該重構了:
重構前:
if (age >= 18) { /* ... */ }
重構後:
const MIN_ADULT_AGE = 18
if (age >= MIN_ADULT_AGE) { /* ... */ }
重構前:
function calc(x: number) { return x * 0.1 }
重構後:
function calculateTax(price: number) {
const TAX_RATE = 0.1
return price * TAX_RATE
}
Vitest 讓 TDD 變得更簡潔、更直觀:
npm test -- --watch
在重構階段,TypeScript 能幫助我們安全地重構:
export function isPrime(n: number): boolean { // 明確的型別
// 改變函數簽名時,TypeScript 會提醒哪些地方需要修改
}
與傳統測試框架相比,Vitest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。
完整 src/math/mathUtils.ts
:
export function isPrime(n: number): boolean {
if (n < 2) return false
if (n === 2) return true
if (n % 2 === 0) return false
for (let i = 3; i * i <= n; i += 2) {
if (n % i === 0) return false
}
return true
}
完整 tests/day03/math-utils.test.ts
:
import { describe, it, expect } from 'vitest'
import { isPrime } from '../../src/math/mathUtils'
describe('math utilities', () => {
describe('isPrime prime detection', () => {
it('identifies small prime numbers', () => {
expect(isPrime(2)).toBe(true)
expect(isPrime(3)).toBe(true)
expect(isPrime(5)).toBe(true)
})
it('identifies small composite numbers', () => {
expect(isPrime(4)).toBe(false)
expect(isPrime(6)).toBe(false)
expect(isPrime(9)).toBe(false)
})
it('handles boundary cases', () => {
expect(isPrime(0)).toBe(false)
expect(isPrime(1)).toBe(false)
expect(isPrime(-1)).toBe(false)
})
it('handles larger prime numbers', () => {
expect(isPrime(11)).toBe(true)
expect(isPrime(13)).toBe(true)
})
})
})
TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。
TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。
試著用 TDD 方式實作一個 isEven
函數:
記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!
明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。